import JSON5 from 'json5';
import {
  AbsolutePath,
  ProjectDependenciesManager,
  FS,
  DescriptiveError,
} from '../core';
import Mustache from 'mustache';
import Case from 'case';
import { Logger } from '../io';

type Version = string;
type DependencySpecifier = `file:${string}` | Version;

export type AutolinkingConfig = {
  harmonyProjectPath: AbsolutePath;
  nodeModulesPath: AbsolutePath;
  ohPackagePathRelativeToHarmony: string;
  etsRNOHPackagesFactoryPathRelativeToHarmony: string;
  cppRNOHPackagesFactoryPathRelativeToHarmony: string;
  cmakeAutolinkPathRelativeToHarmony: string;
  excludedNpmPackageNames: Set<string>;
  includedNpmPackageNames: Set<string>;
};

type AutolinkableLibrary = {
  npmPackageName: string;
  harFilePathRelativeToHarmony: string;
  etsRNOHPackageClassName?: string;
  cppRNOHPackageClassName?: string;
  cmakeLibraryTargetName?: string;
  ohPackageName?: string;
};

type AutolinkingInput = {
  projectRootPath: AbsolutePath;
  etsRNOHPackagesFactoryPath: AbsolutePath;
  cppRNOHPackagesFactoryPath: AbsolutePath;
  cmakeAutolinkingPath: AbsolutePath;
  ohPackagePathAndContent: [AbsolutePath, string];
  autolinkableLibraries: AutolinkableLibrary[];
  nodeModuleHarFilePathsRelativeToHarmony: string[];
  skippedLibraryNpmPackageNames: string[];
};

type AutolinkingOutput = {
  linkedLibraryNpmPackageNames: string[];
  skippedLibraryNpmPackageNames: string[];
  projectRootPath: AbsolutePath;
  etsRNOHPackagesFactoryPathAndContent: [AbsolutePath, string];
  cppRNOHPackagesFactoryPathAndContent: [AbsolutePath, string];
  cmakeAutolinkingPathAndContent: [AbsolutePath, string];
  ohPackagePathAndContent: [AbsolutePath, string];
};

const ETS_RNOH_PACKAGES_FACTORY_TEMPLATE = `
/*
 * This file was generated by RNOH autolinking.
 * DO NOT modify it manually, your changes WILL be overwritten.
 */
import type { RNPackageContext, RNOHPackage } from '@rnoh/react-native-openharmony';
{{#libraries}}
import {{etsRNOHPackageClassName}} from '{{{ohPackageName}}}';
{{/libraries}}

export function createRNOHPackages(ctx: RNPackageContext): RNOHPackage[] {
  return [
{{#libraries}}
    new {{etsRNOHPackageClassName}}(ctx),
{{/libraries}}
  ];
}
`.trimStart();

const CPP_RNOH_PACKAGES_FACTORY_TEMPLATE = `
/*
 * This file was generated by RNOH autolinking.
 * DO NOT modify it manually, your changes WILL be overwritten.
 */
// clang-format off
#pragma once
#include "RNOH/Package.h"
{{#libraries}}
#include "{{etsRNOHPackageClassName}}.h"
{{/libraries}}

std::vector<rnoh::Package::Shared> createRNOHPackages(const rnoh::Package::Context &ctx) {
  return {
{{#libraries}}
    std::make_shared<rnoh::{{cppRNOHPackageClassName}}>(ctx),
{{/libraries}}
  };
}
`.trimStart();

const CMAKE_AUTOLINKING_TEMPLATE = `
# This file was generated by RNOH autolinking.
# DO NOT modify it manually, your changes WILL be overwritten.
cmake_minimum_required(VERSION 3.5)

# @api
function(autolink_libraries target)
{{#libraries}}
    add_subdirectory("\${OH_MODULES_DIR}/{{{ohPackageName}}}/src/main/cpp" ./{{cmakeLibraryTargetName}})
{{/libraries}}

    set(AUTOLINKED_LIBRARIES
{{#libraries}}
        {{cmakeLibraryTargetName}}
{{/libraries}}
    )

    foreach(lib \${AUTOLINKED_LIBRARIES})
        target_link_libraries(\${target} PUBLIC \${lib})
    endforeach()
endfunction()
`.trimStart();

export class Autolinking {
  constructor(private fs: FS, private logger: Logger) {}

  async prepareInput(config: AutolinkingConfig): Promise<AutolinkingInput> {
    if (
      config.includedNpmPackageNames.size > 0 &&
      config.excludedNpmPackageNames.size > 0
    ) {
      throw new DescriptiveError({
        whatHappened: 'Tried to exclude and include npm packages.',
        whatCanUserDo: {
          default: ['Include or exclude npm packages, but not both.'],
        },
        extraData: {
          includedNpmPackageNamesCount: config.includedNpmPackageNames.size,
          excludedNpmPackageNamesCount: config.excludedNpmPackageNames.size,
          includedNpmPackageNames: config.includedNpmPackageNames,
          excludedNpmPackageNames: config.excludedNpmPackageNames,
        },
      });
    }
    const harmonyProjectPath =
      config.harmonyProjectPath ?? new AbsolutePath('./');
    const nodeModuleHarPaths: AbsolutePath[] = [];
    const autolinkableLibraries: AutolinkableLibrary[] = [];
    const projectRootPath = config.nodeModulesPath.copyWithNewSegment('..');
    const skippedLibraryNpmPackageNames: string[] = [];
    await new ProjectDependenciesManager(this.fs, projectRootPath).forEachAsync(
      (dependency) => {
        const harFilePath = dependency.getHarFilePath();
        if (!harFilePath) {
          return;
        }
        nodeModuleHarPaths.push(harFilePath);
        const packageJson = dependency.readPackageJSON();
        const providedAutolinkingConfig = packageJson.harmony?.autolinking;

        if (
          providedAutolinkingConfig === undefined ||
          providedAutolinkingConfig === null ||
          (config.excludedNpmPackageNames.has(packageJson.name) &&
            config.excludedNpmPackageNames.size > 0) ||
          (!config.includedNpmPackageNames.has(packageJson.name) &&
            config.includedNpmPackageNames.size > 0)
        ) {
          skippedLibraryNpmPackageNames.push(packageJson.name);
          return;
        }
        const autolinkingConfig =
          providedAutolinkingConfig === true ? {} : providedAutolinkingConfig;
        autolinkableLibraries.push({
          npmPackageName: packageJson.name,
          etsRNOHPackageClassName: autolinkingConfig?.etsPackageClassName,
          cppRNOHPackageClassName: autolinkingConfig?.cppPackageClassName,
          cmakeLibraryTargetName: autolinkingConfig?.cmakeLibraryTargetName,
          ohPackageName: autolinkingConfig?.ohPackageName,
          harFilePathRelativeToHarmony: harFilePath
            .relativeTo(harmonyProjectPath)
            .toString(),
        });
      }
    );
    const ohPackagePath = harmonyProjectPath.copyWithNewSegment(
      config.ohPackagePathRelativeToHarmony
    );
    const ohPackageContent = await this.fs.readText(ohPackagePath);
    const cppRNOHPackagesFactoryPath = harmonyProjectPath.copyWithNewSegment(
      config.cppRNOHPackagesFactoryPathRelativeToHarmony
    );
    const cmakeAutolinkingPath = harmonyProjectPath.copyWithNewSegment(
      config.cmakeAutolinkPathRelativeToHarmony
    );
    return {
      projectRootPath,
      skippedLibraryNpmPackageNames,
      etsRNOHPackagesFactoryPath: harmonyProjectPath.copyWithNewSegment(
        config.etsRNOHPackagesFactoryPathRelativeToHarmony
      ),
      cppRNOHPackagesFactoryPath,
      cmakeAutolinkingPath: cmakeAutolinkingPath,
      ohPackagePathAndContent: [ohPackagePath, ohPackageContent],
      autolinkableLibraries,
      nodeModuleHarFilePathsRelativeToHarmony: nodeModuleHarPaths.map((p) =>
        p.relativeTo(harmonyProjectPath).toString()
      ),
    };
  }

  evaluate(input: AutolinkingInput): AutolinkingOutput {
    const autolinkableLibraries: Required<AutolinkableLibrary>[] =
      input.autolinkableLibraries.map((lib) => {
        return {
          ...lib,
          etsRNOHPackageClassName:
            lib.etsRNOHPackageClassName ??
            this.npmPackageNameToRNOHPackageClassName(lib.npmPackageName),
          cppRNOHPackageClassName:
            lib.cppRNOHPackageClassName ??
            this.npmPackageNameToRNOHPackageClassName(lib.npmPackageName),
          cmakeLibraryTargetName:
            lib.cmakeLibraryTargetName ??
            this.npmPackageNameToCMakeLibraryTargetName(lib.npmPackageName),
          ohPackageName:
            lib.ohPackageName ??
            this.npmPackageNameToOHPackageName(lib.npmPackageName),
        };
      });
    autolinkableLibraries.sort((a, b) => {
      if (a.npmPackageName < b.npmPackageName) return -1;
      if (a.npmPackageName > b.npmPackageName) return 1;
      return 0;
    });
    const ohPackage = JSON5.parse(input.ohPackagePathAndContent[1]);
    const autolinkableHarFilePathsRelativeToHarmony = autolinkableLibraries.map(
      (lib) => lib.harFilePathRelativeToHarmony
    );
    const unmanagedNativeDependencySpecifierByName: Record<
      string,
      DependencySpecifier
    > = {};
    Object.entries<DependencySpecifier>(ohPackage.dependencies).forEach(
      ([name, dependencySpecifier]) => {
        if (!dependencySpecifier.includes('file:')) {
          unmanagedNativeDependencySpecifierByName[name] = dependencySpecifier;
        } else {
          const harFilePathRelativeToHarmony = dependencySpecifier.replace(
            'file:',
            ''
          );
          if (
            !autolinkableHarFilePathsRelativeToHarmony.includes(
              harFilePathRelativeToHarmony
            ) &&
            input.nodeModuleHarFilePathsRelativeToHarmony.includes(
              harFilePathRelativeToHarmony
            )
          ) {
            unmanagedNativeDependencySpecifierByName[name] =
              dependencySpecifier;
          }
        }
      }
    );
    const managedNativeDependencySpecifierByName: Record<string, string> = {};
    for (const library of autolinkableLibraries) {
      managedNativeDependencySpecifierByName[library.ohPackageName] =
        'file:' + library.harFilePathRelativeToHarmony;
    }
    return {
      skippedLibraryNpmPackageNames: input.skippedLibraryNpmPackageNames,
      linkedLibraryNpmPackageNames: autolinkableLibraries.map(
        (lib) => lib.npmPackageName
      ),
      projectRootPath: input.projectRootPath,
      cppRNOHPackagesFactoryPathAndContent: [
        input.cppRNOHPackagesFactoryPath,
        Mustache.render(CPP_RNOH_PACKAGES_FACTORY_TEMPLATE, {
          libraries: autolinkableLibraries,
        }),
      ],
      etsRNOHPackagesFactoryPathAndContent: [
        input.etsRNOHPackagesFactoryPath,
        Mustache.render(ETS_RNOH_PACKAGES_FACTORY_TEMPLATE, {
          libraries: autolinkableLibraries,
        }),
      ],
      cmakeAutolinkingPathAndContent: [
        input.cmakeAutolinkingPath,
        Mustache.render(CMAKE_AUTOLINKING_TEMPLATE, {
          libraries: autolinkableLibraries,
        }),
      ],
      ohPackagePathAndContent: [
        input.ohPackagePathAndContent[0],
        JSON5.stringify(
          {
            ...ohPackage,
            dependencies: {
              ...unmanagedNativeDependencySpecifierByName,
              ...managedNativeDependencySpecifierByName,
            },
          },
          { space: 2, quote: '"' }
        ) + '\n',
      ],
    };
  }

  private npmPackageNameToRNOHPackageClassName(fullNpmPackageName: string) {
    if (fullNpmPackageName.startsWith('@')) {
      const [scopeName, packageName] = fullNpmPackageName
        .replace('@', '')
        .split('/');
      return Case.pascal(scopeName) + Case.pascal(packageName) + 'Package';
    }
    return Case.pascal(fullNpmPackageName) + 'Package';
  }

  private npmPackageNameToCMakeLibraryTargetName(fullNpmPackageName: string) {
    if (fullNpmPackageName.startsWith('@')) {
      const [scopeName, packageName] = fullNpmPackageName
        .replace('@', '')
        .split('/');
      return 'rnoh__' + Case.snake(scopeName) + '__' + Case.snake(packageName);
    }
    return 'rnoh__' + Case.snake(fullNpmPackageName);
  }

  private npmPackageNameToOHPackageName(fullNpmPackageName: string): string {
    if (fullNpmPackageName.startsWith('@')) {
      const [scopeName, packageName] = fullNpmPackageName
        .replace('@', '')
        .split('/');
      return '@rnoh/' + Case.kebab(scopeName) + '--' + Case.kebab(packageName);
    }
    return '@rnoh/' + Case.kebab(fullNpmPackageName);
  }

  saveAndLogOutput(output: AutolinkingOutput) {
    const pathAndContentPairs: [AbsolutePath, string][] = [];
    pathAndContentPairs.push(output.etsRNOHPackagesFactoryPathAndContent);
    pathAndContentPairs.push(output.cppRNOHPackagesFactoryPathAndContent);
    pathAndContentPairs.push(output.cmakeAutolinkingPathAndContent);
    pathAndContentPairs.push(output.ohPackagePathAndContent);
    pathAndContentPairs.forEach(([path, content]) => {
      this.fs.writeTextSync(path, content);
    });
    output.linkedLibraryNpmPackageNames.forEach((npmPackageName) => {
      this.logger.info(
        (styles) => `[${styles.green(styles.bold('link'))}] ${npmPackageName}`
      );
    });
    output.skippedLibraryNpmPackageNames.forEach((npmPackageName) => {
      this.logger.warn(
        (styles) => `[${styles.yellow(styles.bold('skip'))}] ${npmPackageName}`
      );
    });
    this.logger.info(() => '');
    const sortedPathsRelativeToRoot = pathAndContentPairs
      .map(([path]) => path.relativeTo(output.projectRootPath).toString())
      .sort();
    sortedPathsRelativeToRoot.forEach((pathStr) => {
      this.logger.info((styles) => styles.gray(`• ${pathStr}`));
    });
    this.logger.info(() => '');
    this.logger.info(
      (styles) =>
        `updated ${styles.green(
          styles.bold(sortedPathsRelativeToRoot.length)
        )} file(s), linked ${styles.green(
          styles.bold(output.linkedLibraryNpmPackageNames.length)
        )} libraries, skipped ${styles.yellow(
          styles.bold(output.skippedLibraryNpmPackageNames.length)
        )} libraries`,
      { prefix: true }
    );
    this.logger.info(() => '');
  }
}